/*
 * Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc.
 * Copyright (C) 2016 Furrtek
 * copyleft 2024 zxkmm AKA zix aka sommermorgentraum
 *
 * This file is part of PortaPack.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2, or (at your option)
 * any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; see the file COPYING.  If not, write to
 * the Free Software Foundation, Inc., 51 Franklin Street,
 * Boston, MA 02110-1301, USA.
 */

#include "lcd_ili9341.hpp"
#include "bmp.hpp"

#include "portapack_io.hpp"
using namespace portapack;

#include "utility.hpp"

#include "ch.h"

#include "file.hpp"

#include "portapack_persistent_memory.hpp"

#include <complex>

#include <cstring>
#include <string>

namespace lcd {

namespace {

void lcd_reset() {
    io.lcd_reset_state(false);
    chThdSleepMilliseconds(1);
    io.lcd_reset_state(true);
    chThdSleepMilliseconds(10);
    io.lcd_reset_state(false);
    chThdSleepMilliseconds(120);
}

void lcd_sleep_in() {
    io.lcd_data_write_command_and_data(0x10, {});
    // "It will be necessary to wait 5msec before sending next command,
    // this is to allow time for the supply voltages and clock circuits
    // to stabilize."
    chThdSleepMilliseconds(5);
}

void lcd_sleep_out() {
    io.lcd_data_write_command_and_data(0x11, {});
    // "It will be necessary to wait 120msec after sending Sleep Out
    // command (when in Sleep In Mode) before Sleep In command can be
    // sent."
    chThdSleepMilliseconds(120);
}

void lcd_display_on() {
    io.lcd_data_write_command_and_data(0x29, {});
}

void lcd_display_off() {
    io.lcd_data_write_command_and_data(0x28, {});
}

void lcd_sleep(bool hw_sleep = true) {
    lcd_display_off();
    if (hw_sleep) {
        lcd_sleep_in();
    }
}

void lcd_wake(bool hw_sleep = true) {
    if (hw_sleep) {
        lcd_sleep_out();
    }
    lcd_display_on();
}

uint32_t lcd_read_display_status() {
    io.lcd_data_write_command_and_data(0x09, {});
    io.lcd_read_word();

    uint32_t value2 = io.lcd_read_word();
    uint32_t value3 = io.lcd_read_word();
    uint32_t value4 = io.lcd_read_word();
    uint32_t value5 = io.lcd_read_word();
    return value5 + (value4 << 8) + (value3 << 16) + (value2 << 24);
}

void lcd_init() {
    // LCDs are configured for IM[2:0] = 001
    // 8080-I system, 16-bit parallel bus

    io.lcd_data_write_command_and_data(0xE0, {0x00, 0x09, 0x0C, 0x03, 0x10, 0x06, 0x34, 0x68,
                                              0x49, 0x02, 0x0A, 0x07, 0x2C, 0x31, 0x0F});

    io.lcd_data_write_command_and_data(0xE1, {0x00, 0x12, 0x15, 0x02, 0x10, 0x06, 0x35, 0x35,
                                              0x4A, 0x05, 0x10, 0x0C, 0x2F, 0x33, 0x0F});

    io.lcd_data_write_command_and_data(0xC0, {0x0F, 0x0F});  // Vreg1out=4.5 Vreg2out=-4.5

    io.lcd_data_write_command_and_data(0xC1, {0x44});  // VGH = 5*VCI VGL = -4*VCI

    io.lcd_data_write_command_and_data(0xC5, {0x00, 0x66, 0x80});  // VCOM

    io.lcd_data_write_command_and_data(0x36, {
                                                 (1 << 7) |  // MY=1
                                                 (0 << 6) |  // MX=0
                                                 (0 << 5) |  // MV=0
                                                 (1 << 4) |  // ML=1: reverse vertical refresh to simplify scrolling logic
                                                 (1 << 3)    // BGR=1: For Kingtech LCD, BGR filter.
                                             });

    // io.lcd_data_write_command_and_data(0x36, {0x48});
    io.lcd_data_write_command_and_data(0x3A, {0x65});  // 0x55 was the original
    // 这里的刷新率过低？
    // io.lcd_data_write_command_and_data(0xB1, {0x40, 0x1F});  // fix 60 fps
    io.lcd_data_write_command_and_data(0xB1, {0xA0, 0x11});  // Frame rate source is 11fps

    io.lcd_data_write_command_and_data(0xB4, {0x02});  // 2 dot inversion

    io.lcd_data_write_command_and_data(0xEE, {0x00, 0x04});

    io.lcd_data_write_command_and_data(0xE9, {0x00});

    io.lcd_data_write_command_and_data(0xF7, {0xA9, 0x51, 0x2C, 0x82});

    io.lcd_data_write_command_and_data(0x21, {});  // Display Inversion On

    io.lcd_data_write_command_and_data(0x11, {});  // Sleep Out
    chThdSleepMilliseconds(120);                   // Delay 120ms

    io.lcd_data_write_command_and_data(0x29, {});  // Display On
    chThdSleepMilliseconds(50);                    // Delay 50ms

    // Turn on Tearing Effect Line (TE) output signal.
    io.lcd_data_write_command_and_data(0x35, {0b00000000});
}

void lcd_init_pp() {
    // LCDs are configured for IM[2:0] = 001
    // 8080-I system, 16-bit parallel bus

    //
    // 0x3a: DBI[2:0] = 101
    // MDT[1:0] = XX (if not in 18-bit mode, right?)

    // Power control B
    // 0
    // PCEQ=1, DRV_ena=0, Power control=3
    io.lcd_data_write_command_and_data(0xCF, {0x00, 0xD9, 0x30});

    // Power on sequence control
    io.lcd_data_write_command_and_data(0xED, {0x64, 0x03, 0x12, 0x81});

    // Driver timing control A
    io.lcd_data_write_command_and_data(0xE8, {0x85, 0x10, 0x78});

    // Power control A
    io.lcd_data_write_command_and_data(0xCB, {0x39, 0x2C, 0x00, 0x34, 0x02});

    // Pump ratio control
    io.lcd_data_write_command_and_data(0xF7, {0x20});

    // Driver timing control B
    io.lcd_data_write_command_and_data(0xEA, {0x00, 0x00});

    io.lcd_data_write_command_and_data(0xB1, {0x00, 0x1B});

    // Blanking Porch Control
    // VFP = 0b0000010 = 2 (number of HSYNC of vertical front porch)
    // VBP = 0b0000010 = 2 (number of HSYNC of vertical back porch)
    // HFP = 0b0001010 = 10 (number of DOTCLOCK of horizontal front porch)
    // HBP = 0b0010100 = 20 (number of DOTCLOCK of horizontal back porch)
    io.lcd_data_write_command_and_data(0xB5, {0x02, 0x02, 0x0a, 0x14});

    // Display Function Control
    // PT[1:0] = 0b10
    // PTG[1:0] = 0b10
    // ISC[3:0] = 0b0010 (scan cycle interval of gate driver: 5 frames)
    // SM = 0 (gate driver pin arrangement in combination with GS)
    // SS = 1 (source output scan direction S720 -> S1)
    // GS = 0 (gate output scan direction G1 -> G320)
    // REV = 1 (normally white)
    // NL = 0b100111 (default)
    // PCDIV = 0b000000 (default?)

    /*as per the datasheet chapter 8.3.7, addr B6h,
    data "REV" bit, liquid crystal type:*/
    if (portapack::persistent_memory::config_lcd_normally_black())
        io.lcd_data_write_command_and_data(0xB6, {0x0A, 0x22, 0x27, 0x00});  // IPS : normally black : 0
    else
        io.lcd_data_write_command_and_data(0xB6, {0x0A, 0xA2, 0x27, 0x00});  // TFT : normally white : 1

    // Power Control 1
    // VRH[5:0]
    io.lcd_data_write_command_and_data(0xC0, {0x1B});

    // Power Control 2
    // SAP[2:0];BT[3:0]
    io.lcd_data_write_command_and_data(0xC1, {0x12});

    // VCOM Control 1
    io.lcd_data_write_command_and_data(0xC5, {0x32, 0x3C});

    // VCOM Control 2
    io.lcd_data_write_command_and_data(0xC7, {0x9B});

    // Memory Access Control
    // Invert X and Y memory access order, so upper-left of
    // screen is (0,0) when writing to display.
    io.lcd_data_write_command_and_data(0x36, {
                                                 (1 << 7) |  // MY=1
                                                 (1 << 6) |  // MX=1
                                                 (0 << 5) |  // MV=0
                                                 (1 << 4) |  // ML=1: reverse vertical refresh to simplify scrolling logic
                                                 (1 << 3)    // BGR=1: For Kingtech LCD, BGR filter.
                                             });

    // COLMOD: Pixel Format Set
    // DPI=101 (16 bits/pixel), DBI=101 (16 bits/pixel)
    io.lcd_data_write_command_and_data(0x3A, {0x55});

    // io.lcd_data_write_command_and_data(0xF6, { 0x01, 0x30 });
    //  WEMODE=1 (reset column and page number on overflow)
    //  MDT[1:0]
    //  EPF[1:0]=00 (use channel MSB for LSB)
    //  RIM=0 (If COLMOD[6:4]=101 (65k color), 16-bit RGB interface (1 transfer/pixel))
    //  RM=0 (system interface/VSYNC interface)
    //  DM[1:0]=00 (internal clock operation)
    //  ENDIAN=0 (doesn't matter with 16-bit interface)
    io.lcd_data_write_command_and_data(0xF6, {0x01, 0x30, 0x00});

    // 3Gamma Function Disable
    io.lcd_data_write_command_and_data(0xF2, {0x00});

    // Gamma curve selected
    io.lcd_data_write_command_and_data(0x26, {0x01});

    // Set Gamma
    io.lcd_data_write_command_and_data(0xE0, {0x0F, 0x1D, 0x19, 0x0E, 0x10, 0x07, 0x4C, 0x63,
                                              0x3F, 0x03, 0x0D, 0x00, 0x26, 0x24, 0x04});

    // Set Gamma
    io.lcd_data_write_command_and_data(0xE1, {0x00, 0x1C, 0x1F, 0x02, 0x0F, 0x03, 0x35, 0x25,
                                              0x47, 0x04, 0x0C, 0x0B, 0x29, 0x2F, 0x05});

    lcd_wake();

    // Turn on Tearing Effect Line (TE) output signal.
    io.lcd_data_write_command_and_data(0x35, {0b00000000});
}

void lcd_set(const uint_fast8_t command, const uint_fast16_t start, const uint_fast16_t end) {
    io.lcd_data_write_command_and_data(command, {static_cast<uint8_t>(start >> 8), static_cast<uint8_t>(start & 0xff),
                                                 static_cast<uint8_t>(end >> 8), static_cast<uint8_t>(end & 0xff)});
}

void lcd_ramwr_start() {
    io.lcd_data_write_command_and_data(0x2c, {});
}

void lcd_ramrd_start() {
    io.lcd_data_write_command_and_data(0x2e, {});
    io.lcd_read_word();
}

void lcd_caset(const uint_fast16_t start_column, uint_fast16_t end_column) {
    lcd_set(0x2a, start_column, end_column);
}

void lcd_paset(const uint_fast16_t start_page, const uint_fast16_t end_page) {
    lcd_set(0x2b, start_page, end_page);
}

void lcd_start_ram_write(
    const ui::Point p,
    const ui::Size s) {
    lcd_caset(p.x(), p.x() + s.width() - 1);
    lcd_paset(p.y(), p.y() + s.height() - 1);
    lcd_ramwr_start();
    io.update_cached_values();
}

void lcd_start_ram_read(
    const ui::Point p,
    const ui::Size s) {
    lcd_caset(p.x(), p.x() + s.width() - 1);
    lcd_paset(p.y(), p.y() + s.height() - 1);
    lcd_ramrd_start();
}

void lcd_start_ram_write(
    const ui::Rect& r) {
    lcd_start_ram_write(r.location(), r.size());
}

void lcd_start_ram_read(
    const ui::Rect& r) {
    lcd_start_ram_read(r.location(), r.size());
}

void lcd_vertical_scrolling_definition(
    const uint_fast16_t top_fixed_area,
    const uint_fast16_t vertical_scrolling_area,
    const uint_fast16_t bottom_fixed_area) {
    io.lcd_data_write_command_and_data(0x33, {static_cast<uint8_t>(top_fixed_area >> 8),
                                              static_cast<uint8_t>(top_fixed_area & 0xff),
                                              static_cast<uint8_t>(vertical_scrolling_area >> 8),
                                              static_cast<uint8_t>(vertical_scrolling_area & 0xff),
                                              static_cast<uint8_t>(bottom_fixed_area >> 8),
                                              static_cast<uint8_t>(bottom_fixed_area & 0xff)});
}

void lcd_vertical_scrolling_start_address(
    const uint_fast16_t vertical_scrolling_pointer) {
    io.lcd_data_write_command_and_data(0x37, {static_cast<uint8_t>(vertical_scrolling_pointer >> 8),
                                              static_cast<uint8_t>(vertical_scrolling_pointer & 0xff)});
}

}  // namespace

uint32_t ILI9341::lcd_read_display_id() {
    io.lcd_data_write_command_and_data(0x04, {});
    io.lcd_read_data_raw();

    uint32_t value3 = io.lcd_read_data_raw();
    uint32_t value4 = io.lcd_read_data_raw();
    uint32_t value5 = io.lcd_read_data_raw();
    return value5 + (value4 << 8) + (value3 << 16);
}

bool ILI9341::read_display_status() {
    lcd_reset();
    uint32_t display_status = lcd_read_display_status();

    /* This tries to validate the display_status.
     * The value could vary from device to device, so we are less specific here.
     * 0xFFFFFEFF was seen when the display was not reachable
     * 0x00610000 was seen when the display was reachable
     */

    if (display_status > 0x0E000000ULL)
        return false;

    if (display_status < 0x00000100ULL)
        return false;

    return true;
}

void ILI9341::init() {
    lcd_reset();
    bool hpp = false;
    // detect method 1
    uint32_t id = lcd_read_display_id();
    if (id == 5537894) hpp = true;

    // detect method 2 - not working!
    if (!hpp) {
        draw_pixel({318, 478}, ui::Color::red());
        fill_rectangle_unrolled8({0, 0, 240, 320}, ui::Color::black());
        std::vector<ui::ColorRGB888> checker(1);
        read_pixels({318, 478, 1, 1}, checker.data(), 1);
        if (checker[0].b <= 10 && checker[0].g <= 10 && checker[0].r >= 100)
            hpp = true;
    }
    // init screen
    if (hpp) {
        device_type = DEV_PORTARF;
        lcd_init();
        screen_width = 320;
        screen_height = 480;
    } else {
        device_type = DEV_PORTAPACK;
        lcd_init_pp();
        screen_width = 240;
        screen_height = 320;
    }
}

void ILI9341::shutdown() {
    lcd_reset();
}

void ILI9341::sleep(bool hw_sleep) {
    lcd_sleep(hw_sleep);
}

void ILI9341::wake(bool hw_sleep) {
    lcd_wake(hw_sleep);
}

void ILI9341::fill_rectangle(ui::Rect r, const ui::Color c) {
    const auto r_clipped = r.intersect(screen_rect());
    if (!r_clipped.is_empty()) {
        lcd_start_ram_write(r_clipped);
        size_t count = r_clipped.width() * r_clipped.height();
        io.lcd_write_pixels(c, count);
    }
}

void ILI9341::fill_rectangle_unrolled8(ui::Rect r, const ui::Color c) {
    const auto r_clipped = r.intersect(screen_rect());
    if (!r_clipped.is_empty()) {
        lcd_start_ram_write(r_clipped);
        size_t count = r_clipped.width() * r_clipped.height();
        io.lcd_write_pixels_unrolled8(c, count);
    }
}

void ILI9341::render_line(const ui::Point p, const uint16_t count, const ui::Color* line_buffer) {
    lcd_start_ram_write(p, {count, 1});
    io.lcd_write_pixels(line_buffer, count);
}

void ILI9341::render_box(const ui::Point p, const ui::Size s, const ui::Color* line_buffer) {
    lcd_start_ram_write(p, s);
    io.lcd_write_pixels(line_buffer, s.width() * s.height());
}

// RLE_4 BMP loader (delta not implemented)
/* draw transparent, pass transparent color as arg, usage inline anonymous obj
 * portapack::display.draw_bmp_from_bmp_hex_arr({100, 100}, foo_bmp, (const uint8_t[]){41, 24, 22}); // dec, out of {255, 255, 255}
 * portapack::display.draw_bmp_from_bmp_hex_arr({100, 100}, foo_bmp, (const uint8_t[]){0x29, 0x18, 0x16}); //hex out of {0xFF, 0xFF, 0xFF}
 *
 * draw transparent, pass transparent color as arg, usage pass uint8_t[] obj
 * const uint8_t c[] = {41, 24, 22}; or const uint8_t c[3] = {0x29, 0x18, 0x16};
 * portapack::display.draw_bmp_from_bmp_hex_arr({100, 100}, foo_bmp, c);
 *
 * don't draw transparent, pass nullptr as arg, usage
 * portapack::display.draw_bmp_from_bmp_hex_arr({100, 100}, foo_bmp, nullptr);
 *
 * if your image use RLE compress as transparency methods, pass any valid color as arg, it doesn't matter (like the modal bmp. TODO: write RLE transparency generator)
 * */
void ILI9341::draw_bmp_from_bmp_hex_arr(const ui::Point p, const uint8_t* bitmap, const uint8_t* transparency_color) {
    const bmp_header_t* bmp_header = (const bmp_header_t*)bitmap;
    uint32_t data_idx;
    uint8_t by, c, count, transp_idx = 0;
    ui::Color line_buffer[240];
    uint16_t px = 0, py;
    ui::Color palette[16];

    // Abort if bad depth or no RLE
    if ((bmp_header->bpp != 4) ||
        (bmp_header->compression != 2)) return;

    data_idx = bmp_header->image_data;
    const bmp_palette_t* bmp_palette = (const bmp_palette_t*)&bitmap[bmp_header->BIH_size + 14];

    // Convert palette and find pure magenta index (alpha color key) rgb dec(41,24,22)
    for (c = 0; c < 16; c++) {
        palette[c] = ui::Color(bmp_palette->color[c].R, bmp_palette->color[c].G, bmp_palette->color[c].B);
        if (transparency_color &&
            (bmp_palette->color[c].R == transparency_color[0]) &&
            (bmp_palette->color[c].G == transparency_color[1]) &&
            (bmp_palette->color[c].B == transparency_color[2])) transp_idx = c;
    }

    if (!transparency_color) {
        py = bmp_header->height + 16;
        do {
            by = bitmap[data_idx++];
            if (by) {
                count = by >> 1;
                by = bitmap[data_idx++];
                for (c = 0; c < count; c++) {
                    line_buffer[px++] = palette[by >> 4];
                    if (px < bmp_header->width) line_buffer[px++] = palette[by & 15];
                }
                if (data_idx & 1) data_idx++;
            } else {
                by = bitmap[data_idx++];
                if (by == 0) {
                    render_line({p.x(), p.y() + py}, bmp_header->width, line_buffer);
                    py--;
                    px = 0;
                } else if (by == 1) {
                    break;
                } else if (by == 2) {
                    // Delta
                } else {
                    count = by >> 1;
                    for (c = 0; c < count; c++) {
                        by = bitmap[data_idx++];
                        line_buffer[px++] = palette[by >> 4];
                        if (px < bmp_header->width) line_buffer[px++] = palette[by & 15];
                    }
                    if (data_idx & 1) data_idx++;
                }
            }
        } while (1);
    } else {
        py = bmp_header->height;
        do {
            by = bitmap[data_idx++];
            if (by) {
                count = by >> 1;
                by = bitmap[data_idx++];
                for (c = 0; c < count; c++) {
                    if ((by >> 4) != transp_idx) draw_pixel({static_cast<ui::Coord>(p.x() + px), static_cast<ui::Coord>(p.y() + py)}, palette[by >> 4]);
                    px++;
                    if (px < bmp_header->width) {
                        if ((by & 15) != transp_idx) draw_pixel({static_cast<ui::Coord>(p.x() + px), static_cast<ui::Coord>(p.y() + py)}, palette[by & 15]);
                    }
                    px++;
                }
                if (data_idx & 1) data_idx++;
            } else {
                by = bitmap[data_idx++];
                if (by == 0) {
                    py--;
                    px = 0;
                } else if (by == 1) {
                    break;
                } else if (by == 2) {
                    // Delta
                } else {
                    count = by >> 1;
                    for (c = 0; c < count; c++) {
                        by = bitmap[data_idx++];
                        if ((by >> 4) != transp_idx) draw_pixel({static_cast<ui::Coord>(p.x() + px), static_cast<ui::Coord>(p.y() + py)}, palette[by >> 4]);
                        px++;
                        if (px < bmp_header->width) {
                            if ((by & 15) != transp_idx) draw_pixel({static_cast<ui::Coord>(p.x() + px), static_cast<ui::Coord>(p.y() + py)}, palette[by & 15]);
                        }
                        px++;
                    }
                    if (data_idx & 1) data_idx++;
                }
            }
        } while (1);
    }
}

/* Draw BMP from SD card.
 * Currently supported formats:
 *     16bpp ARGB, RGB565
 *     24bpp RGB
 *     32bpp ARGB
 */
bool ILI9341::draw_bmp_from_sdcard_file(const ui::Point p, const std::filesystem::path& file) {
    File bmpimage;
    size_t file_pos = 0;
    uint16_t pointer = 0;
    int16_t px = 0, py, width, height;
    bmp_header_t bmp_header;
    uint8_t type = 0;
    char buffer[257];
    ui::Color line_buffer[320];
    int16_t start_x = p.x();
    auto result = bmpimage.open(file);
    if (result.is_valid())
        return false;

    bmpimage.seek(file_pos);
    auto read_size = bmpimage.read(&bmp_header, sizeof(bmp_header));
    if (!((bmp_header.signature == 0x4D42) &&                               // "BM" Signature
          (bmp_header.planes == 1) &&                                       // Seems always to be 1
          (bmp_header.compression == 0 || bmp_header.compression == 3))) {  // No compression
        return false;
    }

    switch (bmp_header.bpp) {
        case 16:
            file_pos = 0x36;
            memset(buffer, 0, 16);
            bmpimage.read(buffer, 16);
            if (buffer[1] == 0x7C)
                type = 3;  // A1R5G5B5
            else
                type = 0;  // R5G6B5
            break;
        case 24:
            type = 1;
            break;
        case 32:
        default:
            type = 2;
            break;
    }

    width = bmp_header.width;
    height = bmp_header.height;
    start_x = (screen_width - start_x - width) / 2 + start_x;  // center horizontally
    if (width > screen_width || width > 320)
        return false;

    file_pos = bmp_header.image_data;

    py = height + 16 - 1;
    /*                 ^ this is for to "start" AKA "image end" draw at the 17th line,
     *                   because the render_line logic below is start with p.y() + py until "end" AKA "image start"*/

    while (1) {
        while (px < width) {
            bmpimage.seek(file_pos);
            memset(buffer, 0, 257);
            read_size = bmpimage.read(buffer, 256);
            if (read_size.is_error())
                return false;  // Read error

            pointer = 0;
            while (pointer < 256) {
                if (pointer + 4 > 256)
                    break;
                switch (type) {
                    case 0:  // R5G6B5
                    case 3:  // A1R5G5B5
                        if (!type)
                            line_buffer[px] = ui::Color((uint16_t)buffer[pointer] | ((uint16_t)buffer[pointer + 1] << 8));
                        else
                            line_buffer[px] = ui::Color(((uint16_t)buffer[pointer] & 0x1F) | ((uint16_t)buffer[pointer] & 0xE0) << 1 | ((uint16_t)buffer[pointer + 1] & 0x7F) << 9);
                        pointer += 2;
                        file_pos += 2;
                        break;
                    case 1:  // 24
                    default:
                        line_buffer[px] = ui::Color(buffer[pointer + 2], buffer[pointer + 1], buffer[pointer]);
                        pointer += 3;
                        file_pos += 3;
                        break;
                    case 2:  // 32
                        line_buffer[px] = ui::Color(buffer[pointer + 2], buffer[pointer + 1], buffer[pointer]);
                        pointer += 4;
                        file_pos += 4;
                        break;
                }
                px++;
                if (px >= width) {
                    break;
                }
            }
            if (read_size.value() != 256)
                break;
        }
        render_line({start_x, p.y() + py}, px, line_buffer);
        px = 0;
        py--;

        if (read_size.value() < 256 || py < 0)
            break;
    }
    return true;
}

void ILI9341::draw_line(const ui::Point start, const ui::Point end, const ui::Color color) {
    int x0 = start.x();
    int y0 = start.y();
    int x1 = end.x();
    int y1 = end.y();

    int dx = std::abs(x1 - x0), sx = x0 < x1 ? 1 : -1;
    int dy = std::abs(y1 - y0), sy = y0 < y1 ? 1 : -1;
    int err = (dx > dy ? dx : -dy) / 2, e2;

    for (;;) {
        draw_pixel({static_cast<ui::Coord>(x0), static_cast<ui::Coord>(y0)}, color);
        if (x0 == x1 && y0 == y1) break;
        e2 = err;
        if (e2 > -dx) {
            err -= dy;
            x0 += sx;
        }
        if (e2 < dy) {
            err += dx;
            y0 += sy;
        }
    }
}

void ILI9341::fill_circle(
    const ui::Point center,
    const ui::Dim radius,
    const ui::Color foreground,
    const ui::Color background) {
    const uint32_t radius2 = radius * radius;
    for (int32_t y = -radius; y < radius; y++) {
        const int32_t y2 = y * y;
        for (int32_t x = -radius; x < radius; x++) {
            const int32_t x2 = x * x;
            const uint32_t d2 = x2 + y2;
            const bool inside = d2 < radius2;
            const auto color = inside ? foreground : background;
            draw_pixel({x + center.x(), y + center.y()}, color);
        }
    }
}

void ILI9341::draw_pixel(
    const ui::Point p,
    const ui::Color color) {
    if (screen_rect().contains(p)) {
        lcd_start_ram_write(p, {1, 1});
        io.lcd_write_pixel(color);
    }
}

void ILI9341::draw_pixels(
    const ui::Rect r,
    const ui::Color* const colors,
    const size_t count) {
    /* TODO: Assert that rectangle width x height < count */
    lcd_start_ram_write(r);
    io.lcd_write_pixels(colors, count);
}

void ILI9341::read_pixels(
    const ui::Rect r,
    ui::ColorRGB888* const colors,
    const size_t count) {
    /* TODO: Assert that rectangle width x height < count */
    lcd_start_ram_read(r);
    io.lcd_read_bytes(
        reinterpret_cast<uint8_t*>(colors),
        count * sizeof(ui::ColorRGB888));
}

void ILI9341::draw_bitmap(
    const ui::Point p,
    const ui::Size size,
    const uint8_t* const pixels,
    const ui::Color foreground,
    const ui::Color background,
    uint8_t zoom_level) {
    if (zoom_level <= 1) {
        // Not a transparent background
        if (ui::Color::magenta().v != background.v) {
            lcd_start_ram_write(p, size);

            const size_t count = size.width() * size.height();
            for (size_t i = 0; i < count; i++) {
                const auto pixel = pixels[i >> 3] & (1U << (i & 0x7));
                io.lcd_write_pixel(pixel ? foreground : background);
            }
        } else {
            // transparent bg
            int x = p.x();
            int y = p.y();
            int maxX = x + size.width();
            const size_t count = size.width() * size.height();
            for (size_t i = 0; i < count; i++) {
                const auto pixel = pixels[i >> 3] & (1U << (i & 0x7));
                if (pixel) {
                    if (x <= screen_width && y <= screen_height) draw_pixel(ui::Point(x, y), foreground);
                }
                // move to next px
                x++;
                if (x >= maxX) {
                    x = p.x();
                    y++;
                }
            }
        }
    } else {  // zoom

        // dot to square
        for (int y = 0; y < size.height(); y++) {
            for (int x = 0; x < size.width(); x++) {
                // pos
                size_t bit_index = y * size.width() + x;
                int byte_index = bit_index >> 3;
                int bit_pos = bit_index & 0x7;

                // val
                const auto pixel = pixels[byte_index] & (1U << bit_pos);
                /*                 ^ the byte_index-th bit AKA current bit
                                                         ^ current px in current byte*/

                if (pixel || background.v != ui::Color::magenta().v) {
                    fill_rectangle(
                        {p.x() + x * zoom_level, p.y() + y * zoom_level,
                         zoom_level, zoom_level},
                        pixel ? foreground : background);
                }
            }
        }
    }
}

void ILI9341::draw_glyph(
    const ui::Point p,
    const ui::Glyph& glyph,
    const ui::Color foreground,
    const ui::Color background,
    uint8_t zoom_level) {
    draw_bitmap(p, glyph.size(), glyph.pixels(), foreground, background, zoom_level);
}

void ILI9341::scroll_set_area(
    const ui::Coord top_y,
    const ui::Coord bottom_y) {
    scroll_state.top_area = top_y;
    scroll_state.bottom_area = height() - bottom_y;
    scroll_state.height = bottom_y - top_y;
    lcd_vertical_scrolling_definition(scroll_state.top_area, scroll_state.height, scroll_state.bottom_area);
}

ui::Coord ILI9341::scroll_set_position(
    const ui::Coord position) {
    scroll_state.current_position = position % scroll_state.height;
    const uint_fast16_t address = scroll_state.top_area + scroll_state.current_position;
    lcd_vertical_scrolling_start_address(address);
    return address;
}

ui::Coord ILI9341::scroll(const int32_t delta) {
    return scroll_set_position(scroll_state.current_position + scroll_state.height - delta);
}

ui::Coord ILI9341::scroll_area_y(const ui::Coord y) const {
    const auto wrapped_y = (scroll_state.current_position + y) % scroll_state.height;
    return wrapped_y + scroll_state.top_area;
}

void ILI9341::scroll_disable() {
    if (device_type == DEV_PORTAPACK) {
        lcd_vertical_scrolling_definition(0, height(), 0);
        lcd_vertical_scrolling_start_address(0);
    } else {
        io.lcd_data_write_command_and_data(0x13, {});  // normal mode
    }
}

} /* namespace lcd */
